Challenge 1¶

1. Basics of Image and Signal Processing (LE1)¶

1.1. Image Properties¶

Day 1¶

Find or acquire 1-3 images related to your selected country Mexic0. The images should be suitable for demonstrating adjustments to image properties brightness and hue in experiments.

The pictures are from when i was living in Mexico.

repository at https://github.com/BR4GR/gbsv-challenges

code writen with the help of the Deep Dive repo chatgpt and opencv.org

In [1]:
import cv2 as cv
import librosa
import matplotlib.pyplot as plt
import numpy as np
import sounddevice as sd
import time as time1

from ipywidgets import interact, widgets
from matplotlib import pyplot as plt
from matplotlib.widgets import Slider, Button, TextBox
from scipy.io.wavfile import write
In [2]:
jellyfish = cv.imread('img/jellyfish.jpg')
print(f"{jellyfish.shape=}\n{jellyfish.dtype=}\n{np.max(jellyfish)=}\n{np.min(jellyfish)=}")
jellyfish = cv.cvtColor(jellyfish, cv.COLOR_BGR2RGB)

plt.imshow(jellyfish)
plt.show()

cavediving = cv.imread('img/cavediving.jpg')
print(f"{cavediving.shape=}\n{cavediving.dtype=}\n{np.max(cavediving)=}\n{np.min(cavediving)=}")
cavediving = cv.cvtColor(cavediving, cv.COLOR_BGR2RGB)

plt.imshow(cavediving)
plt.show()

sunrise = cv.imread('img/sunrise.jpg')
print(f"{sunrise.shape=}\n{sunrise.dtype=}\n{np.max(sunrise)=}\n{np.min(sunrise)=}")
sunrise = cv.cvtColor(sunrise, cv.COLOR_BGR2RGB)
plt.imshow(sunrise)
plt.show()
jellyfish.shape=(3648, 5472, 3)
jellyfish.dtype=dtype('uint8')
np.max(jellyfish)=np.uint8(255)
np.min(jellyfish)=np.uint8(0)
No description has been provided for this image
cavediving.shape=(3648, 5472, 3)
cavediving.dtype=dtype('uint8')
np.max(cavediving)=np.uint8(255)
np.min(cavediving)=np.uint8(0)
No description has been provided for this image
sunrise.shape=(4000, 6000, 3)
sunrise.dtype=dtype('uint8')
np.max(sunrise)=np.uint8(255)
np.min(sunrise)=np.uint8(0)
No description has been provided for this image
In [3]:
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# Function to adjust brightness for an RGB image
def adjust_brightness(image, brightness=0):
    """
    Adjust the brightness of an RGB image.
    
    Parameters:
    - image: Input image in RGB format
    - brightness: Amount to adjust brightness (can be positive or negative)
    
    Returns:
    - Brightness-adjusted image
    """
    image = np.int16(image)
    image = image + brightness
    image = np.clip(image, 0, 255)  # Ensure values stay within [0, 255]
    return np.uint8(image)

def adjust_hue(image, hue_shift=0):
    """
    Adjust the hue of an RGB image.
    
    Parameters:
    - image: Input image in RGB format
    - hue_shift: Amount to shift the hue by (in degrees)
    
    Returns:
    - Hue-adjusted image
    """
    hsv_image = cv.cvtColor(image, cv.COLOR_RGB2HSV)
    hue = hsv_image[:, :, 0].astype(int)
    hue = (hue + hue_shift) % 180  # Ensure hue stays within the range [0, 179]
    hsv_image[:, :, 0] = hue.astype(np.uint8)
    return cv.cvtColor(hsv_image, cv.COLOR_HSV2RGB)

def apply_adjustments(image, brightness=0, hue_shift=0):
    """
    Apply brightness and hue adjustments to an RGB image.
    
    Parameters:
    - image: Input image in RGB format
    - brightness: Amount to adjust brightness
    - hue_shift: Amount to shift the hue by

    Returns:
    - Image with brightness and hue adjustments applied
    """
    bright_image = adjust_brightness(image, brightness)
    final_image = adjust_hue(bright_image, hue_shift)
    return final_image

# Function to display the original and adjusted images side by side
def display_images_side_by_side(image, brightness=0, hue_shift=0, ax=None, title="Image"):
    """
    Display the original and adjusted images side by side for comparison.
    
    Parameters:
    - image: The input image in RGB format
    - brightness: Amount to adjust brightness
    - hue_shift: Amount to shift the hue by
    """
    adjusted_image = apply_adjustments(image, brightness, hue_shift)
    
    # Original image
    ax[0].imshow(image)
    ax[0].set_title(f'Original {title}')
    ax[0].axis('off')
    
    # Adjusted image
    ax[1].imshow(adjusted_image)
    ax[1].set_title(f'Adjusted {title} (Brightness: {brightness}, Hue: {hue_shift})')
    ax[1].axis('off')

def display_all_images():
    """
    Display all images with fixed adjustments for brightness and hue.
    """
    # Fixed values for brightness and hue for each image
    brightness_jellyfish = -40
    hue_jellyfish = 40
    
    brightness_cave = -10
    hue_cave = 20
    
    brightness_sunrise = 10
    hue_sunrise = -20

    fig, axes = plt.subplots(3, 2, figsize=(12, 12))

    display_images_side_by_side(jellyfish, brightness_jellyfish, hue_jellyfish, axes[0], "jellyfish")
    display_images_side_by_side(cavediving, brightness_cave, hue_cave, axes[1], "cave diving")
    display_images_side_by_side(sunrise, brightness_sunrise, hue_sunrise, axes[2], "sunrise")
    
    plt.tight_layout()
    plt.savefig("img/adjusted_images2x3.png", dpi=300)
    plt.show()

display_all_images()
No description has been provided for this image

Day 2¶

Define a problem and use case related to your country profile (Steckbrief) for each assigned property that you intend to address (brightness and hue). Then define 1-2 experiments with objectives on how you want to address your defined problem and use case. Multiple experiments/objectives can be analyzed in one single image. Write concisely for each experiment: WHAT problem and use case do you address, HOW do you want to address/solve the problem for this specific use case, and WHY is your experiment helpful for the chosen use case?

Problem:¶

The jellyfish image was taken in a aquarium in playa del Carmen, the room was extreemly dark even thought it was full of different changing colorfull lights, which ilumnated the jelyfish. the image is too bright, and the variety of colors from the light show is not well represented. The dark room and colorful glowing effect of the jellyfish are lost due to overexposure. This results in the background becoming too visible and reduces the visual focus on the jellyfish, which were meant to glow against a dark backdrop.

Use Case:¶

Simulating the original light show where jellyfish glowed in different colors in a dark room. The objective is to recreate the low-light ambiance and simulate the various colors the jellyfish displayed during the light show using hue adjustments.

Objectives of the Experiments:¶

Brightness Adjustment: Reduce the overall brightness to restore the dark environment, ensuring that the jellyfish glow remains the visual focus. How: Lower the brightness to darken the background while keeping the jellyfish visible, mimicking the darkroom atmosphere. we will test and compare different methodes of brightness ajustment, like gamma, region speciffic brightness, or clahe.

Hue Adjustment:* Simulate the changing colors of the light show by adjusting the hue. This allows us to create the effect of different colored lights shining on the jellyfish, even though the image only shows one color. How: Shift the hue to enhance the jellyfish’s glow and replicate the dynamic color changes seen during the light show.

Explanation, Why are these adjustments relevant?¶

Brightness: The dark room is an essential part of the jellyfish exhibit, where overexposure distorts the intended visual experience. Reducing brightness restores this intended ambiance.

Hue: The light show was dynamic and colorful, and adjusting the hue allows us to simulate the color transitions that were key to the experience. This makes the image more visually accurate and engaging.

Day 3¶

Conduct part 1 of your experiments with your images and suitable methods. Reason your choice of parameters and methods using visualizations or 1-2 sentences emach.

In [4]:
def adjust_gamma(image, gamma=1.0):
    """
    Adjust the gamma correction of an image.

    Parameters:
    - image: Input image in RGB format
    - gamma: Gamma value for correction (default is 1.0)
    
    Returns:
    - Gamma-corrected image
    """
    inv_gamma = 1.0 / gamma
    table = np.array([(i / 255.0) ** inv_gamma * 255 for i in np.arange(0, 256)]).astype("uint8")
    return cv.LUT(image, table)

def apply_region_specific_brightness(image, brightness_value):
    """
    Apply region-specific brightness adjustment to an image.

    Parameters:
    - image: Input image in RGB format
    - brightness_value: Amount to adjust brightness (can be positive or negative)
    
    Returns:
    - Image with region-specific brightness adjustment applied
    """
    mask = image[:, :, 2] < 100  # Apply brightness adjustment to the darker regions only
    brightened_image = adjust_brightness(image.copy(), brightness_value)
    image[mask] = brightened_image[mask]
    return image

def apply_clahe(image, clip_limit=2.0, tile_grid_size=(8, 8)):
    """
    Apply Contrast Limited Adaptive Histogram Equalization (CLAHE) to an image.
    
    Parameters:
    - image: Input image in RGB format
    - clip_limit: Threshold for contrast limiting
    - tile_grid_size: Size of grid for histogram equalization

    Returns:
    - Image with CLAHE applied
    """
    lab_image = cv.cvtColor(image, cv.COLOR_RGB2LAB)
    clahe = cv.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
    lab_image[:, :, 0] = clahe.apply(lab_image[:, :, 0])
    return cv.cvtColor(lab_image, cv.COLOR_LAB2RGB)


def apply_vignette(image, radius_factor=2, brightness_reduction=0.5):
    """
    Apply a vignette effect to an image.

    Parameters:
    - image: Input image in RGB format
    - radius_factor: Factor to control the size of the vignette
    - brightness_reduction: Amount to reduce brightness at the edges

    Returns:
    - Image with vignette effect applied
    """
    rows, cols = image.shape[:2]
    center_x, center_y = cols // 2, rows // 2
    # Create a Gaussian kernel that will serve as the vignette mask
    # Calculate the maximum possible radius
    max_radius = np.sqrt(center_x ** 2 + center_y ** 2) / radius_factor
    # Create meshgrid for the X and Y coordinates
    x = np.arange(cols)
    y = np.arange(rows)
    X, Y = np.meshgrid(x, y)
    # Calculate the distance from the center of the image
    distance_from_center = np.sqrt((X - center_x) ** 2 + (Y - center_y) ** 2)
    # Normalize the distances to a range between 0 and 1, then reverse (1 at center, 0 at edges)
    vignette_mask = np.clip(1 - (distance_from_center / max_radius), 0, 1)
    # Apply brightness reduction (e.g., 0.5 means reduce brightness by 50% at the edges)
    vignette_mask = vignette_mask ** brightness_reduction  # Adjust the strength of reduction
    # Apply the vignette mask to each channel (RGB)
    vignette_mask_3d = vignette_mask[:, :, np.newaxis]  # Make mask compatible with 3-channel image
    vignette_image = np.uint8(image * vignette_mask_3d)
    return vignette_image


# Experiment: Create three levels of brightness adjustment (80%, 60%, 40%)
brightness_levels = [-15, -30, -45]
gamma_values = [0.85, 0.7, 0.55]
vignette_values = [0.85, 0.7, 0.55]
clahe_clip_limits = [1.0, 2.0, 3.0]

# Create a 3x3 grid for each method and level of adjustment
fig, ax = plt.subplots(5, 3, figsize=(15, 13))

# Row 1: Linear brightness adjustment
for i, brightness in enumerate(brightness_levels):
    adjusted_image = adjust_brightness(jellyfish.copy(), brightness)
    ax[0, i].imshow(adjusted_image)
    ax[0, i].set_title(f'Linear Brightness: {100 + brightness}%')
    ax[0, i].axis('off')

# Row 3: Region-specific brightness adjustment
for i, brightness in enumerate(brightness_levels):
    region_adjusted_image = apply_region_specific_brightness(jellyfish.copy(), brightness)
    ax[1, i].imshow(region_adjusted_image)
    ax[1, i].set_title(f'Region-Specific: {100 + brightness}%')
    ax[1, i].axis('off')
    
# Row 2: Gamma correction
for i, gamma in enumerate(gamma_values):
    gamma_corrected_image = adjust_gamma(jellyfish.copy(), gamma)
    ax[2, i].imshow(gamma_corrected_image)
    ax[2, i].set_title(f'Gamma Correction: {gamma}')
    ax[2, i].axis('off')

# Row 4: CLAHE with different clip limits (strengths)
for i, clip_limit in enumerate(clahe_clip_limits):
    clahe_image = apply_clahe(jellyfish.copy(), clip_limit=clip_limit)
    ax[3, i].imshow(clahe_image)
    ax[3, i].set_title(f'CLAHE: Clip Limit = {clip_limit}')
    ax[3, i].axis('off')

# Row 1: Linear brightness adjustment
for i, brightness in enumerate(vignette_values):
    adjusted_image = apply_vignette(jellyfish.copy(), 1, brightness)
    ax[4, i].imshow(adjusted_image)
    ax[4, i].set_title(f'vignette Correction: {brightness}')
    ax[4, i].axis('off')


plt.tight_layout()
plt.savefig("img/brightnessexperiment.png", dpi=300)
plt.show()
No description has been provided for this image

Reasoning for Methods and Parameters:¶

Gamma Correction was chosen for its ability to apply non-linear adjustments, which better handles darkening shadows while preserving highlights, making it the best method for enhancing the contrast between the glowing jellyfish and the dark background. CLAHE was chosen for localized contrast enhancement, particularly in low-contrast areas (background). Region-specific adjustments were used to target specific areas like the background, maintaining the focus on the jellyfish. The vignette effect was used as a creative approach to enhance the vintage feel and control the outer brightness. Parameters were chosen by trial and error, using large and small stept untill a godd step size was found for each Method.

Observations:¶

The best result was achieved with Gamma Correction (0.55), which effectively darkened the background while preserving the vibrant, glowing jellyfish. The vignette effect added a vintage feel but wasn't the best fit for the goal of simulating the aquarium light show.

In [5]:
dark_adjusted_jellyfish = adjust_gamma(jellyfish.copy(), 0.55)

# Display the original and adjusted images side by side
fig, ax = plt.subplots(1, 2, figsize=(12, 6))
ax[0].imshow(jellyfish)
ax[0].set_title('Original Jellyfish')
ax[0].axis('off')

ax[1].imshow(dark_adjusted_jellyfish)
ax[1].set_title('Adjusted Jellyfish: Gamma = 0.55')
ax[1].axis('off')
cv.imwrite("img/dark_adjusted_jellyfish.png", cv.cvtColor(dark_adjusted_jellyfish, cv.COLOR_RGB2BGR))
plt.tight_layout()
plt.savefig("img/dark_adjusted_jellyfish_comparison.png", dpi=300)
plt.show()
No description has been provided for this image

Day 4¶

Conduct part 2 of your experiments with your images and suitable methods. Reason your choice of parameters and methods using visualizations or 1-2 sentences each.

In [6]:
hue_shifts = [-80, -60, -40, -20, 0, 20, 40, 60, 80]

# Create a 3x3 grid for hue adjustments
fig, ax = plt.subplots(3, 3, figsize=(12, 12))

for i, hue_shift in enumerate(hue_shifts):
    hue_adjusted_image = adjust_hue(dark_adjusted_jellyfish.copy(), hue_shift=hue_shift)
    
    row = i // 3
    col = i % 3
    
    ax[row, col].imshow(hue_adjusted_image)
    ax[row, col].set_title(f'Hue Shift: {hue_shift}°')
    ax[row, col].axis('off')

plt.tight_layout()
plt.savefig("img/hue_experiment.png", dpi=300)
plt.show()
No description has been provided for this image
In [7]:
# Apply hue shifts to different quadrants of the image
def apply_quadrant_hue_shifts(image, hue_shifts):
    """
    Apply different hue shifts to each quadrant of the image.

    Parameters:
    - image: Input image in RGB format
    - hue_shifts: List of hue shifts for each quadrant (in degrees)
    """
    # Define the quadrants
    #jellyfish.shape is (3648, 5472, 3)
    rowsplit = 2000
    columnsplit = 3150

    top_left = image[0:rowsplit, 0:columnsplit]
    top_right = image[0:rowsplit, columnsplit:]
    bottom_right = image[rowsplit:, columnsplit:]
    bottom_left = image[rowsplit:, 0:columnsplit]
    
    # Apply hue shifts to each quadrant
    top_left = adjust_hue(top_left, hue_shifts[0])
    top_right = adjust_hue(top_right, hue_shifts[1])
    bottom_right = adjust_hue(bottom_right, hue_shifts[2])
    bottom_left = adjust_hue(bottom_left, hue_shifts[3])
    
    # Reconstruct the full image with the adjusted quadrants
    top_half = np.hstack((top_left, top_right))
    bottom_half = np.hstack((bottom_left, bottom_right))
    adjusted_image = np.vstack((top_half, bottom_half))
    filename = f"img/quadrant_hue_shifts{hue_shifts[0]}_{hue_shifts[1]}_{hue_shifts[2]}_{hue_shifts[3]}.png"
    cv.imwrite(filename, cv.cvtColor(adjusted_image, cv.COLOR_RGB2BGR))
    print(f"Image saved as {filename}")

apply_quadrant_hue_shifts(dark_adjusted_jellyfish.copy(), [0, 40, 80, 120])
apply_quadrant_hue_shifts(dark_adjusted_jellyfish.copy(), [20, 60, 100, 140])
Image saved as img/quadrant_hue_shifts0_40_80_120.png
Image saved as img/quadrant_hue_shifts20_60_100_140.png

Hue shifts 0, 40, 80 and 120¶

awesome Image

Hue shifts 20, 60, 100 and 140¶

awesome Image

In [8]:
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# Function to apply smooth sliding hue shift from 0° to 180° across the image
def apply_smooth_sliding_hue_shift(image):
    """
    Apply a smooth sliding hue shift from 0° to 180° across the image, left to right.
    
    Parameters:
    - image: Input RGB image.
    
    Returns:
    - Image with a smooth sliding hue shift applied from left to right.
    """
    hsv_image = cv.cvtColor(image, cv.COLOR_RGB2HSV)
    cols = image.shape[1]
    
    # Create a sliding hue mask based on the column position
    for col in range(cols):
        # Compute the hue shift smoothly as a proportion of the column position
        hue_shift = int((col / cols) * 90)  # Scale hue shift between 0 and 90
        hsv_image[:, col, 0] = (hsv_image[:, col, 0] + hue_shift) % 180  # Apply the hue shift to the column
    # Convert back to RGB
    sliding_hue_image = cv.cvtColor(hsv_image, cv.COLOR_HSV2RGB)
    
    return sliding_hue_image

smooth_sliding_hue_image = apply_smooth_sliding_hue_shift(dark_adjusted_jellyfish.copy())
cv.imwrite("img/smooth_sliding_hue_shift_0_to_90.png", cv.cvtColor(smooth_sliding_hue_image, cv.COLOR_RGB2BGR))
Out[8]:
True

awesome Image

Reasoning for Methods and Parameters:¶

The uniform hue shifts were chosen to clearly visualize how different color adjustments affect the overall image. This method allowed for a straightforward comparison of how each hue shift impacts the brightness and color of the jellyfish.

The quadrant-based hue shift and th sliding shift was chosen mostly to see what it looks like, and because the different effects are closer together without whitespaces in between. the parameters where chosen to display the biggest amont of differnt shifts, as there was not really atarget shift. in the show there where also all colors present.

Observations:¶

The results resemble the effect in real live better than i expected, it pretty much looks exactly how i remember it. The gif looks way worse than expected the problem is gif uses les color. as mp4 it looks much better.

In [9]:
# Function to resize image to a smaller resolution using Lanczos interpolation
def resize_image(image, width=1000):
    """
    Resize the image while maintaining the aspect ratio, using Lanczos interpolation for better color quality.
    
    Parameters:
    - image: Input image in RGB format
    - width: The desired width of the output image
    
    Returns:
    - Resized image with improved interpolation
    """
    height = int(image.shape[0] * (width / image.shape[1]))  # Maintain aspect ratio
    # Use Lanczos interpolation for better quality when resizing
    return cv.resize(image, (width, height), interpolation=cv.INTER_LANCZOS4)

# Resize the image using the updated function
resized_image = resize_image(dark_adjusted_jellyfish.copy(), width=1000)


# Create frames for the GIF
def generate_hue_shift_frames(image, output_folder, num_frames=30):
    """
    Generate frames for a smooth hue shift animation.

    Parameters:
    - image: Input image in RGB format
    - output_folder: Folder to save the frames
    - num_frames: Number of frames to generate

    Returns:
    - List of frame paths
    """
    step = 180 // num_frames  # Define the hue step size
    frames = []
    
    for i in range(0, 180, step):
        hue_shifted_image = adjust_hue(image.copy(), i)
        frame_path = f"{output_folder}/frame_{i}.png"
        cv.imwrite(frame_path, cv.cvtColor(hue_shifted_image, cv.COLOR_RGB2BGR))  # Save frame
        frames.append(frame_path)
    
    return frames

# Generate frames for the resized image
frames = generate_hue_shift_frames(resized_image, output_folder='hue_frames', num_frames=60)
In [10]:
from PIL import Image

# Create a GIF from the saved PNG frames with dithering
def create_gif(frames, output_path, duration=100):
    images = [Image.open(frame) for frame in frames]
    
    # Save the GIF with dithering enabled to help smooth color transitions
    images[0].save(
        output_path, 
        save_all=True, 
        append_images=images[1:], 
        duration=duration, 
        loop=0, 
        dither=Image.Dither.FLOYDSTEINBERG,  # Apply dithering
        optimize=False  # Disable further color optimization to avoid additional color loss
    )
    
# Create the GIF with dithering to improve color quality
create_gif(frames, output_path='img/hue_shift_animation_dithered.gif', duration=100)
In [11]:
import cv2
import os

# Function to create an MP4 video from frames
def create_mp4_from_frames(frames, output_path, fps=30):
    # Get the size of the first frame (assuming all frames are the same size)
    frame = cv2.imread(frames[0])
    height, width, layers = frame.shape

    # Define the codec and create a VideoWriter object
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # Define the codec (mp4v for MP4)
    video = cv2.VideoWriter(output_path, fourcc, fps, (width, height))

    # Add each frame to the video
    for frame_path in frames:
        img = cv2.imread(frame_path)
        video.write(img)

    # Release the video writer object
    video.release()

    print(f"MP4 video saved as {output_path}")

# Create the MP4 video from the PNG frames
create_mp4_from_frames(frames, output_path='vid/hue_shift_animation.mp4', fps=30)
MP4 video saved as vid/hue_shift_animation.mp4

GIF¶

Hue Shift Animation

MP4¶

In [12]:
from IPython.display import Video

# Display the video in the notebook
Video("vid/hue_shift_animation.mp4")
Out[12]:
Your browser does not support the video element.

Day 5¶

Measure your results using appropriate methods. Reason your choice of methods and parameters using visualizations or 1-2 sentences. Select specific measurement methods that are particularly suitable for your use case. Analyze the histograms of the original images and, if applicable, during your experiments.

In [13]:
from scipy.stats import skew
from skimage.measure import shannon_entropy

def adjust_brightness(image, brightness=0):
    """
    Adjust the brightness of an RGB image.

    Parameters:
    - image: Input image in RGB format
    - brightness: Amount to adjust brightness (can be positive or negative)

    Returns:
    - Brightness-adjusted image
    """
    image = np.int16(image)
    image = image + brightness
    image = np.clip(image, 0, 255)  # Ensure values stay within [0, 255]
    return np.uint8(image)

# Function to apply gamma correction
def adjust_gamma(image, gamma=1.0):
    """
    Adjust the gamma correction of an image.

    Parameters:
    - image: Input image in RGB format
    - gamma: Gamma value for correction (default is 1.0)

    Returns:
    - Gamma-corrected image
    """
    inv_gamma = 1.0 / gamma
    table = np.array([(i / 255.0) ** inv_gamma * 255 for i in np.arange(0, 256)]).astype("uint8")
    return cv.LUT(image, table)

# Function to plot histograms for brightness adjustments for multiple methods
def plot_multiple_brightness_histograms_log(original_image, methods_dict):
    """
    Plots histograms for the original image and multiple brightness adjustment methods with a logarithmic scale.
    
    Parameters:
    - original_image: The original input image.
    - methods_dict: A dictionary where keys are method names and values are the corresponding adjusted images.
    """
    # Convert original to grayscale
    original_gray = cv.cvtColor(original_image, cv.COLOR_RGB2GRAY)
    
    # Number of methods + 1 for the original image
    num_methods = len(methods_dict)
    
    plt.figure(figsize=(15, 6))
    
    # Plot original image histogram (with log scale on y-axis)
    plt.subplot(1, num_methods + 1, 1)
    plt.hist(original_gray.ravel(), bins=256, range=(0, 256), color='gray')
    plt.yscale('log')  # Set y-axis to logarithmic scale
    plt.title('Original Image (Log Scale)')
    plt.xlabel('Pixel Intensity')
    plt.ylabel('Frequency')
    
    # Plot histograms for each method with log scale
    for i, (method_name, adjusted_image) in enumerate(methods_dict.items(), 2):
        adjusted_gray = cv.cvtColor(adjusted_image, cv.COLOR_RGB2GRAY)
        plt.subplot(1, num_methods + 1, i)
        plt.hist(adjusted_gray.ravel(), bins=256, range=(0, 256), color='gray')
        plt.yscale('log')  # Set y-axis to logarithmic scale
        plt.title(f'{method_name} (Log Scale)')
        plt.xlabel('Pixel Intensity')
    
    plt.tight_layout()
    plt.savefig("brightness_comparison_histograms_log.png", dpi=300)
    plt.show()

def calculate_brightness_kpis(image):
    """
    Calculate Key Performance Indicators (KPIs) for brightness of an image.

    Parameters:
    - image: Input image in RGB format
    
    Returns:
    - Dictionary of KPIs (Mean, Std Dev, Skewness, Dynamic Range, Entropy)
    """
    gray_image = cv.cvtColor(image, cv.COLOR_RGB2GRAY)
    
    # Selected KPIs
    mean = np.mean(gray_image)
    std_dev = np.std(gray_image)
    skewness = skew(gray_image.ravel())
    dynamic_range = np.max(gray_image) - np.min(gray_image)
    entropy = shannon_entropy(gray_image)
    
    return {
        "Mean": mean,
        "Std Dev": std_dev,
        "Skewness": skewness,
        "Dynamic Range": dynamic_range,
        "Entropy": entropy
    }

# Function to calculate KPIs for multiple methods
def compare_brightness_kpis(original_image, methods_dict):
    """
    Compare Key Performance Indicators (KPIs) for brightness of an image across multiple methods.
    
    Parameters:
    - original_image: The original input image.
    - methods_dict: A dictionary where keys are method names and values are the corresponding adjusted images.

    Returns:
    - Dictionary of KPIs for the original image and each adjusted image.
    """
    print("KPIs for the Original Image:")
    original_kpis = calculate_brightness_kpis(original_image)
    for kpi_name, value in original_kpis.items():
        print(f"{kpi_name}: {value}")

    for method_name, adjusted_image in methods_dict.items():
        print(f"\nKPIs for {method_name}:")
        adjusted_kpis = calculate_brightness_kpis(adjusted_image)
        for kpi_name, value in adjusted_kpis.items():
            print(f"{kpi_name}: {value}")


# Example images: original and two different brightness-adjusted methods
original_image = jellyfish.copy()

# Apply different adjustments
gamma_fish = adjust_gamma(original_image.copy(), gamma=0.55)  # Gamma correction
cv.imwrite("img/gamma_fish.png", cv.cvtColor(gamma_fish, cv.COLOR_RGB2BGR))
dark_fish = adjust_brightness(original_image.copy(), brightness=-60)  # Linear brightness adjustment
cv.imwrite("img/dark_fish.png", cv.cvtColor(dark_fish, cv.COLOR_RGB2BGR))



# Step 1: Plot histograms for multiple methods
methods_dict = {
    "Gamma Correction (0.55)": gamma_fish,
    "Linear Brightness (-60)": dark_fish
}

plot_multiple_brightness_histograms_log(original_image, methods_dict)

# Step 2: Compare KPIs for multiple methods
compare_brightness_kpis(original_image, methods_dict)
No description has been provided for this image
KPIs for the Original Image:
Mean: 39.41889767163935
Std Dev: 37.269260229864
Skewness: 1.9444142675661276
Dynamic Range: 254
Entropy: 6.587779365349093

KPIs for Gamma Correction (0.55):
Mean: 15.993016981988047
Std Dev: 27.93977790955635
Skewness: 3.5483630400647086
Dynamic Range: 255
Entropy: 5.137508582224073

KPIs for Linear Brightness (-60):
Mean: 11.016358849597953
Std Dev: 23.555591835182078
Skewness: 3.527971057920704
Dynamic Range: 195
Entropy: 4.078345291811171

Gamma (0.55)¶

awesome Image

Linear Brightness (-60)¶

awesome Image

Why the metrics were chosen: The selected metrics directly address the use case of analyzing brightness adjustments. By using these metrics, we can quantitatively determine how the two adjustment methods (gamma correction and linear brightness adjustment) differ in terms of their effects on the image. Mean and standard deviation were chosen as basic indicators of brightness and contrast. Skewness was chosen to see whether the adjustments cause the image to become more biased towards darker or lighter regions. Dynamic Range was selected to quantify how much detail remains after adjustment. Entropy was chosen to measure how much image information and texture is preserved.

Why specific parameters were chosen: Gamma (0.55) was chosen because it darkens the mid-tones more than the highlights or shadows, which is suitable for the jellyfish image where we want to keep glowing areas intact while darkening the background. Linear Brightness (-60) was chosen to uniformly darken the image, providing a straightforward comparison with the more complex gamma adjustment. Both parameters were found by trial and error.

Day 6¶

Select the best results from your experiments. Summarize observations and interpretations about your results and discuss your findings and results in relation to your problem and use case in approximately 150 words.

Both gamma correction (0.55) and linear brightness adjustment (-60) were applied to simulate a dark environment with glowing jellyfish, similar to the original scene. Upon visual inspection, the gamma correction preserved the overall mid-tone contrast but exaggerated some of the bright spots, especially around the center-left of the image. This caused parts of the image to become overexposed and lose subtle detail. In contrast, the linear brightness adjustment resulted in a more balanced effect where the jellyfish glowed softly, and the bright spots remained controlled, making it a better choice for retaining natural highlights.

From a quantitative standpoint, gamma correction resulted in a higher skewness and slightly higher entropy than linear adjustment. However, entropy alone is not the best indicator of quality in this case, as the linear adjustment kept the bright spots intact and avoided oversaturation, preserving the aesthetic and intended visual effect of the jellyfish glowing in a dark room.

Therefore, linear brightness adjustment is the most suitable method for this specific use case of preserving glowing objects in a dark environment.

Day 7¶

Find or acquire 1-3 signals related to your selected country. The signals should be suitable for demonstrating adjustments to signal properties frequency and smoothing in experiments.

In [14]:
audio, sampling_rate = librosa.load("sound/AztecWhistle.wav")
print(f"{audio=}")
print(f"{audio.dtype=}")
print(f"{audio.shape=}")
print(f"{np.max(audio)=}")
print(f"{np.min(audio)=}")
sd.play(audio, sampling_rate)
audio=array([0.00283813, 0.00253296, 0.00274658, ..., 0.        , 0.        ,
       0.        ], dtype=float32)
audio.dtype=dtype('float32')
audio.shape=(121174,)
np.max(audio)=np.float32(0.9999695)
np.min(audio)=np.float32(-0.9477539)
In [15]:
time = np.arange(0, len(audio)) / sampling_rate
plt.figure(figsize=(12, 4))
plt.plot(time, audio)
plt.title("Original Aztec Death Whistle Signal")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.show()
No description has been provided for this image

The Aztec death whistle is an ancient ceremonial instrument used by the Aztecs, known for producing an intense, eerie sound similar to a human scream. It was often used in rituals and psychological warfare to create fear and panic.

Day 8¶

Define a problem and use case related to your country profile (Steckbrief) for each assigned property (frequency and smoothing) that you intend to address. Then define 1-2 experiments with objectives on how you want to address your defined problem and use case. Multiple experiments/objectives can be analyzed in one single signal. Write concisely for each experiment: WHAT problem and use case do you address, HOW do you want to address/solve the problem for this specific use case, and WHY is your experiment helpful for the chosen use case?

problem and use case

  • Understanding why certain sounds (like the Aztec death whistle) cause emotional and physical reactions can provide insights into how sound has been used in warfare or rituals. Analyzing the whistle’s signal can help us understand the acoustic properties that trigger these reactions, and experimenting with modifications can demonstrate how small changes can reduce or enhance these unsettling characteristics.

Experiment 1: Smoothing the Signal Using Low-Pass Filtering

  • Objective: To smooth the chaotic, sharp transitions in the sound to make the signal less jarring, while observing how smoothing affects the emotional impact of the sound.

  • Method: We will apply low-pass filters with different cutoff frequencies (e.g., 1kHz, 2kHz) to remove the higher, more chaotic frequencies that contribute to the whistle's harshness. This will result in a smoother, softer version of the sound. After applying the filters, we will compare the original and smoothed signals both visually (waveform analysis) and aurally (listening).

  • Why This is Helpful: Smoothing reduces the sharp, abrupt changes in the sound, potentially making the sound less aggressive and more palatable. This experiment will help demonstrate how removing high-frequency content impacts the emotional response to the whistle, making it useful for educational or research purposes where the sound's intensity needs to be reduced.

Experiment 2: Dynamic Frequency Shifting (Frequency Modulation)

  • Objective: To experiment with frequency shifting (modulation) techniques to shift the whistle's frequencies up or down the spectrum and analyze how these shifts alter the sound’s emotional effect.

  • Method: We will apply frequency modulation by shifting the whistle’s base frequencies up and down by 500 Hz, 1000 Hz, and 2000 Hz. This will allow us to experiment with how the pitch-shifting of frequencies changes the emotional impact of the sound.

  • Why This is Helpful: By shifting the whistle’s frequency spectrum, we can explore how changes in pitch impact the listener’s perception. The goal is to see whether lowering or raising the frequencies diminishes or enhances the unsettling quality of the sound.

Experiment 3: Temporal Smoothing with Savitzky-Golay Filter

  • Objective: To smooth the signal over time, reducing the sharp changes in amplitude while maintaining the core characteristics of the whistle’s tone.

  • Method: We will apply a Savitzky-Golay filter, a popular smoothing algorithm, to smooth the time-domain signal. This will help reduce the abrupt transitions in the sound without affecting its frequency components. We will compare the amplitude envelope before and after smoothing to measure the impact on the sound.

  • Why This is Helpful: Smoothing the time-domain signal will reduce the jarring effect of sudden loud noises while retaining the whistle's tonal characteristics. This is especially useful for environments where the sound’s intensity needs to be softened while maintaining its core identity.

Day 9¶

Conduct part 1 of your experiments with your signals and suitable methods. Reason your choice of parameters and methods using visualizations or 1-2 sentences each.

In [16]:
import librosa.effects

# Function to shift the frequencies
def shift_frequencies(audio, sampling_rate, shift_in_hz, direction="up"):
    """
    Shift the frequencies of an audio signal by a specified amount.

    Parameters:
    - audio: Input audio signal
    - sampling_rate: Sampling rate of the audio signal
    - shift_in_hz: Amount to shift the frequencies by (in Hz)
    - direction: Direction of the shift ("up" or "down")

    Returns:
    - Audio signal with shifted frequencies
    """
    if direction == "up":
        shift_in_steps = 12 * np.log2((shift_in_hz + sampling_rate / 2) / (sampling_rate / 2))
    elif direction == "down":
        shift_in_steps = -12 * np.log2((shift_in_hz + sampling_rate / 2) / (sampling_rate / 2))
    
    # Apply the pitch shift
    shifted_audio = librosa.effects.pitch_shift(audio, sr=sampling_rate, n_steps=shift_in_steps)
    return shifted_audio

shifted_1000_up = shift_frequencies(audio, sampling_rate, 1000)
shifted_2000_up = shift_frequencies(audio, sampling_rate, 2000)
shifted_3000_up = shift_frequencies(audio, sampling_rate, 3000)
shifted_1000_down = shift_frequencies(audio, sampling_rate, 1000, direction="down")
shifted_2000_down = shift_frequencies(audio, sampling_rate, 2000, direction="down")
shifted_3000_down = shift_frequencies(audio, sampling_rate, 3000, direction="down")

write('sound/shifted_1000_up.wav', sampling_rate, shifted_1000_up)
write('sound/shifted_2000_up.wav', sampling_rate, shifted_2000_up)
write('sound/shifted_3000_up.wav', sampling_rate, shifted_3000_up)
write('sound/shifted_1000_down.wav', sampling_rate, shifted_1000_down)
write('sound/shifted_2000_down.wav', sampling_rate, shifted_2000_down)
write('sound/shifted_3000_down.wav', sampling_rate, shifted_3000_down)

For the frequency shifting experiment, the method of frequency modulation was chosen because altering the base frequencies of a signal directly impacts how listeners perceive pitch. Frequency modulation allows us to explore whether increasing or decreasing pitch intensifies or reduces the emotional impact, providing insight into the acoustic properties that make the sound disturbing.

The parameters for the frequency shifts were chosen by trial and error.

In [17]:
from IPython.display import Markdown, Audio

# Function to plot a single signal and provide audio playback
def plot_signal_with_audio(audio_signal, title, sampling_rate, audio_label):
    plt.figure(figsize=(10, 4))
    plt.plot(time, audio_signal)
    plt.title(title)
    plt.xlabel("Time (s)")
    plt.ylabel("Amplitude")
    plt.tight_layout()
    plt.show()

    # Display the audio player with a label
    display(Markdown(f"**{audio_label}**"))
    display(Audio(audio_signal, rate=sampling_rate))

# Plot the original signal
plot_signal_with_audio(audio, title="Original Signal", sampling_rate=sampling_rate, audio_label="Original Audio Playback")

# Plot +1000 Hz and -1000 Hz shifted signals
plot_signal_with_audio(shifted_1000_up, title="+1000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="+1000 Hz Shifted Audio Playback")
plot_signal_with_audio(shifted_1000_down, title="-1000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="-1000 Hz Shifted Audio Playback")

# Plot +2000 Hz and -2000 Hz shifted signals
plot_signal_with_audio(shifted_2000_up, title="+2000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="+2000 Hz Shifted Audio Playback")
plot_signal_with_audio(shifted_2000_down, title="-2000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="-2000 Hz Shifted Audio Playback")

# Plot +3000 Hz and -3000 Hz shifted signals
plot_signal_with_audio(shifted_3000_up, title="+3000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="+3000 Hz Shifted Audio Playback")
plot_signal_with_audio(shifted_3000_down, title="-3000 Hz Shifted Signal", sampling_rate=sampling_rate, audio_label="-3000 Hz Shifted Audio Playback")
No description has been provided for this image

Original Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

+1000 Hz Shifted Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

-1000 Hz Shifted Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

+2000 Hz Shifted Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

-2000 Hz Shifted Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

+3000 Hz Shifted Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

-3000 Hz Shifted Audio Playback

Your browser does not support the audio element.

Day 10¶

Conduct part 2 of your experiments with your signals and suitable methods. Reason your choice of parameters and methods using visualizations or 1-2 sentences each.

In [18]:
from IPython.display import Markdown, Audio
from scipy.signal import butter, filtfilt

# Function to apply low-pass filter
def apply_low_pass_filter(signal, sampling_rate, cutoff_hz):
    # Design a butterworth low-pass filter
    nyquist = 0.5 * sampling_rate
    normal_cutoff = cutoff_hz / nyquist
    b, a = butter(N=5, Wn=normal_cutoff, btype='low', analog=False)
    filtered_signal = filtfilt(b, a, signal)
    return filtered_signal

# Apply low-pass filters with different cutoff frequencies
low_pass_1000 = apply_low_pass_filter(audio, sampling_rate, 1000)
low_pass_2000 = apply_low_pass_filter(audio, sampling_rate, 2000)

# Plot the original and low-pass filtered signals with audio playback
def plot_filtered_signals_with_audio():
    plt.figure(figsize=(24, 12))

    # Original signal plot
    plt.subplot(3, 1, 1)
    plt.plot(time, audio)
    plt.title("Original Signal")
    plt.xlabel("Time (s)")
    plt.ylabel("Amplitude")

    # Low-pass filtered (1000 Hz) signal plot
    plt.subplot(3, 1, 2)
    plt.plot(time, low_pass_1000)
    plt.title("Low-Pass Filtered Signal (1000 Hz)")
    plt.xlabel("Time (s)")
    plt.ylabel("Amplitude")

    # Low-pass filtered (2000 Hz) signal plot
    plt.subplot(3, 1, 3)
    plt.plot(time, low_pass_2000)
    plt.title("Low-Pass Filtered Signal (2000 Hz)")
    plt.xlabel("Time (s)")
    plt.ylabel("Amplitude")

    plt.tight_layout()
    plt.show()

    # Display audio players with labels
    display(Markdown("**Original Audio Playback**"))
    display(Audio(audio, rate=sampling_rate))

    display(Markdown("**Low-Pass 1000 Hz Filtered Audio Playback**"))
    display(Audio(low_pass_1000, rate=sampling_rate))

    display(Markdown("**Low-Pass 2000 Hz Filtered Audio Playback**"))
    display(Audio(low_pass_2000, rate=sampling_rate))

# Call the function to plot and display audio
plot_filtered_signals_with_audio()

# Save the filtered audio to files
from scipy.io.wavfile import write
write('sound/low_pass_1000.wav', sampling_rate, low_pass_1000.astype(np.float32))
write('sound/low_pass_2000.wav', sampling_rate, low_pass_2000.astype(np.float32))
No description has been provided for this image

Original Audio Playback

Your browser does not support the audio element.

Low-Pass 1000 Hz Filtered Audio Playback

Your browser does not support the audio element.

Low-Pass 2000 Hz Filtered Audio Playback

Your browser does not support the audio element.
In [19]:
sd.play(low_pass_1000, sampling_rate)
In [20]:
sd.play(audio, sampling_rate)
In [21]:
sd.play(low_pass_2000, sampling_rate)
In [22]:
from scipy.signal import savgol_filter

# Apply Savitzky-Golay filter
def apply_savitzky_golay_filter(signal, window_length=101, polyorder=2):
    return savgol_filter(signal, window_length=window_length, polyorder=polyorder)

# Apply Savitzky-Golay filter
savitzky_golay_smoothed = apply_savitzky_golay_filter(audio)

# Plot the original and Savitzky-Golay smoothed signals
plt.figure(figsize=(12, 6))
plt.subplot(2, 1, 1)
plt.plot(time, audio)
plt.title("Original Signal")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")

plt.subplot(2, 1, 2)
plt.plot(time, savitzky_golay_smoothed)
plt.title("Savitzky-Golay Smoothed Signal")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")

plt.tight_layout()
plt.show()

write('sound/savitzky_golay_smoothed.wav', sampling_rate, savitzky_golay_smoothed.astype(np.float32))
No description has been provided for this image
In [23]:
from IPython.display import Markdown, Audio
import librosa.display

# Function to plot spectrogram with audio and label
def plot_spectrogram_with_audio(audio, sampling_rate, title="Spectrogram", audio_label="Audio Playback"):
    plt.figure(figsize=(10, 4))
    S = librosa.feature.melspectrogram(y=audio, sr=sampling_rate)
    S_dB = librosa.power_to_db(S, ref=np.max)
    librosa.display.specshow(S_dB, sr=sampling_rate, x_axis='time', y_axis='mel', cmap='magma')
    plt.colorbar(format='%+2.0f dB')
    plt.title(title)
    plt.tight_layout()
    
    # Display the audio label and audio player
    display(Markdown(f"**{audio_label}**"))
    display(Audio(audio, rate=sampling_rate))
    
    plt.show()

# Example: Plot original and filtered spectrograms with audio playback
plot_spectrogram_with_audio(audio, sampling_rate, title="Original Signal Spectrogram", audio_label="Original Audio Playback")
plot_spectrogram_with_audio(low_pass_1000, sampling_rate, title="Low-Pass Filtered (1kHz) Spectrogram", audio_label="Low-Pass 1000 Hz Audio Playback")
plot_spectrogram_with_audio(low_pass_2000, sampling_rate, title="Low-Pass Filtered (2kHz) Spectrogram", audio_label="Low-Pass 2000 Hz Audio Playback")
plot_spectrogram_with_audio(savitzky_golay_smoothed, sampling_rate, title="Savitzky-Golay Smoothed Spectrogram", audio_label="Savitzky-Golay Smoothed Audio Playback")

Original Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

Low-Pass 1000 Hz Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

Low-Pass 2000 Hz Audio Playback

Your browser does not support the audio element.
No description has been provided for this image

Savitzky-Golay Smoothed Audio Playback

Your browser does not support the audio element.
No description has been provided for this image
In [25]:
from IPython.display import display, Audio, Markdown
from scipy.signal import hilbert

# Function to plot envelope with audio and label
def plot_envelope_with_audio(audio, sampling_rate, title="Signal Envelope", audio_label="Audio Playback"):
    # Apply Hilbert transform to get the envelope
    analytic_signal = hilbert(audio)
    envelope = np.abs(analytic_signal)
    time = np.arange(len(envelope)) / sampling_rate

    # Plot the envelope
    plt.figure(figsize=(10, 4))
    plt.plot(time, envelope)
    plt.title(title)
    plt.xlabel('Time (s)')
    plt.ylabel('Envelope Amplitude')
    plt.tight_layout()
    plt.show()

    # Display a label for the audio
    display(Markdown(f"**{audio_label}**"))
    # Embed the audio playback directly below the envelope plot
    display(Audio(audio, rate=sampling_rate))

# Example: Plot envelopes and add labeled audio playback for original and filtered signals
plot_envelope_with_audio(audio, sampling_rate, title="Original Signal Envelope", audio_label="Original Audio")
plot_envelope_with_audio(low_pass_1000, sampling_rate, title="Low-Pass Filtered (1kHz) Envelope", audio_label="Low-Pass 1000 Hz Audio")
plot_envelope_with_audio(savitzky_golay_smoothed, sampling_rate, title="Savitzky-Golay Smoothed Signal Envelope", audio_label="Savitzky-Golay Smoothed Audio")
No description has been provided for this image

Original Audio

Your browser does not support the audio element.
No description has been provided for this image

Low-Pass 1000 Hz Audio

Your browser does not support the audio element.
No description has been provided for this image

Savitzky-Golay Smoothed Audio

Your browser does not support the audio element.